/*
** Line Protocol Connection Library
** Copyright (C) 2008 Eric Day
** 
** This program is free software; you can redistribute it and/or
** modify it under the terms of the GNU General Public License
** as published by the Free Software Foundation; either version 2
** of the License, or (at your option) any later version.
** 
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU General Public License for more details.
** 
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to:
** Free Software Foundation, Inc.
** 51 Franklin Street, Fifth Floor,
** Boston, MA  02110-1301, USA
*/

#include "lpc_priv.h"

/*
** static functions
*/

/*
** lpc functions
*/

/* create a new lpc by connecting to server:port */ 
int lpc_connect(LPC **new_lpc, struct pollfd *pfd, char *server, uint16_t port)
{
    int             ret;
    LPC            *lpc;
    struct hostent *host;
    struct in_addr  addr;

    lpc = calloc(1, sizeof(LPC));
    LPC_RET_CHECK(NULL, lpc == NULL, "calloc", errno);

    lpc->pfd = pfd;
    pfd->events = 0;
    pfd->revents = 0;

    *new_lpc = lpc;

    /* first see if server is an IP address, if not, lookup in DNS */
    addr.s_addr = inet_addr(server);
    if(addr.s_addr == -1)
    {
        host = gethostbyname(server);
        LPC_RET_CHECK(lpc, host == NULL, "gethostbyname", 0);

        memcpy((caddr_t)&addr, host->h_addr_list[0], host->h_length);
    }

    lpc->sin.sin_family = AF_INET;
    lpc->sin.sin_addr.s_addr = addr.s_addr;
    lpc->sin.sin_port = htons(port);

    /* call this to start the connection */
    ret = lpc_activity(lpc);
    LPC_RET_CHECK(lpc, ret, "lpc_activity", ret);

    return 0;
}

/* create a new lpc from an existing socket */
int lpc_create(LPC **new_lpc, struct pollfd *pfd, int socket)
{
    int  ret;
    LPC *lpc;

    lpc = calloc(1, sizeof(LPC));
    LPC_RET_CHECK(NULL, lpc == NULL, "calloc", errno);

    lpc->pfd = pfd;
    pfd->fd = socket;
    pfd->events = POLLIN;
    pfd->revents = 0;

    *new_lpc = lpc;

    lpc->flags |= LPC_FLAGS_CONNECTED;

    /* we're using non-blocking sockets */
    ret = fcntl(pfd->fd, F_SETFL, O_NDELAY | O_NONBLOCK);
    if (ret == -1)
        free(lpc);
    LPC_RET_CHECK(NULL, ret == -1, "fcntl", errno);

    return 0;
}

/* create a new lpc that listens on a port */
int lpc_listen(LPC **new_lpc, struct pollfd *pfd, uint16_t port, int backlog)
{
    int  ret;
    int  val;
    LPC *lpc;

    lpc = calloc(1, sizeof(LPC));
    LPC_RET_CHECK(NULL, lpc == NULL, "calloc", errno);

    lpc->pfd = pfd;
    pfd->events = POLLIN;
    pfd->revents = 0;

    *new_lpc = lpc;

    pfd->fd = socket(AF_INET, SOCK_STREAM, 0);
    LPC_RET_CHECK(lpc, pfd->fd == -1, "socket", errno);

    val = 1;
    ret = setsockopt(pfd->fd, SOL_SOCKET, SO_REUSEADDR, (char *)&val,
        sizeof(val));
    LPC_RET_CHECK(lpc, ret == -1, "setsockopt", errno);

    lpc->sin.sin_family = AF_INET;
    lpc->sin.sin_addr.s_addr = INADDR_ANY;
    lpc->sin.sin_port = htons(port);

    ret = bind(pfd->fd, (struct sockaddr *)&(lpc->sin), sizeof(lpc->sin));
    LPC_RET_CHECK(lpc, ret == -1, "bind", errno);

    ret = listen(pfd->fd, backlog);
    LPC_RET_CHECK(lpc, ret == -1, "listen", errno);

    return 0;
}

/* accept a new connection from a listening lpc */
int lpc_accept(LPC *lpc, int *socket)
{
    *socket = accept(lpc->pfd->fd, NULL, 0);
    LPC_RET_CHECK(lpc, *socket == -1, "accept", errno);

    return 0;
}

/* close a lpc */
void lpc_close(LPC *lpc)
{
    lpc->flags &= ~(LPC_FLAGS_CONNECTING | LPC_FLAGS_CONNECTED);

    if (lpc->pfd->fd >= 0)
    {
        close(lpc->pfd->fd);
        lpc->pfd->fd = -1;
    }

    lpc->pfd->events = 0;
}

/* destroy a lpc */
void lpc_destroy(LPC *lpc)
{
    lpc_close(lpc);
    free(lpc);
}

/* take care of socket activity, possibly returning a line */
int lpc_activity(LPC *lpc)
{
    int ret;

    if (!(lpc->flags & LPC_FLAGS_CONNECTED))
    {
        if (!(lpc->flags & LPC_FLAGS_CONNECTING))
        {
            lpc->pfd->fd = socket(AF_INET, SOCK_STREAM, 0);
            LPC_RET_CHECK(lpc, lpc->pfd->fd == -1, "socket", errno);

            ret = fcntl(lpc->pfd->fd, F_SETFL, O_NDELAY | O_NONBLOCK);
            LPC_RET_CHECK(lpc, ret == -1, "fcntl", errno);

            ret = connect(lpc->pfd->fd, (struct sockaddr *)&(lpc->sin),
                sizeof(lpc->sin));
            if (ret == -1 && errno == EINPROGRESS)
            {
                snprintf(lpc->status, LPC_STATUS_LEN, "Connecting");
                lpc->flags |= LPC_FLAGS_CONNECTING;
                lpc->pfd->events = POLLIN | POLLOUT;
                return 0;
            }

            if (ret == -1)
            {
                snprintf(lpc->status, LPC_STATUS_LEN, "Connect Failed (%s)",
                    strerror(errno));
                lpc_close(lpc);
                return 0;
            }
        }

        snprintf(lpc->status, LPC_STATUS_LEN, "Connected");
        lpc->flags &= ~LPC_FLAGS_CONNECTING;
        lpc->flags |= LPC_FLAGS_CONNECTED;
        lpc->pfd->events = POLLIN;
    }

    if (lpc->pfd->revents & POLLIN)
    {
        ret = read(lpc->pfd->fd, lpc->read_buf + lpc->read_len,
            LPC_BUF_LEN - lpc->read_len);
        if (ret == 0 || (ret == -1 && errno != EAGAIN))
        {
            snprintf(lpc->status, LPC_STATUS_LEN, "Read Failed (%s)",
                ret == 0 ? "End of file" : strerror(errno));
            lpc_close(lpc);
            return 0;
        }

        if (ret > 0)
            lpc->read_len += ret;
    }

    if (lpc->pfd->revents & POLLOUT)
    {
        ret = write(lpc->pfd->fd, lpc->write_buf, lpc->write_len);
        if (ret == -1 && errno != EAGAIN)
        {
            snprintf(lpc->status, LPC_STATUS_LEN, "Write Failed (%s)",
                strerror(errno));
            lpc_close(lpc);
            return 0;
        }

        if (ret > 0)
        {
            lpc->write_len -= ret;

            if (lpc->write_len)
            {
                memmove (lpc->write_buf, lpc->write_buf + ret, lpc->write_len);
                lpc->pfd->events = POLLIN | POLLOUT;
            }
        }

        if (lpc->write_len == 0)
            lpc->pfd->events = POLLIN;
    }

    return 0;
}

/* read a line from a lpc */
int lpc_read(LPC *lpc, char *buf, int len)
{
    char *next;
    int   line_len;

    next = memchr(lpc->read_buf, '\n', lpc->read_len);

    /* no newline, so no full command yet. return to read more */
    if(next == NULL)
        return -1;

    /* grab the line out of the buffer, and move whats left to the front */
    line_len = (next - lpc->read_buf);

    if(line_len > 0 && *(next - 1) == '\r')
        line_len--;

    next++;

    if (len > line_len)
        len = line_len;
    else
        len--;

    if (len)
        memcpy(buf, lpc->read_buf, len);

    buf[len] = 0;

    line_len = next - lpc->read_buf;
    lpc->read_len -= line_len;
    memmove(lpc->read_buf, lpc->read_buf + line_len, lpc->read_len);

    return len;
}

/* write a line to a lpc */
int lpc_write(LPC *lpc, char *buf, int len)
{
    if (len > (LPC_BUF_LEN - lpc->write_len) - 2)
        return 0;

    if (len)
    {
        memcpy(lpc->write_buf + lpc->write_len, buf, len);
        lpc->write_len += len;
    }

    memcpy(lpc->write_buf + lpc->write_len, "\r\n", 2);
    lpc->write_len += 2;

    lpc->pfd->events = POLLIN | POLLOUT;

    return 0;
}

/* printf a line to a lpc */
int lpc_printf(LPC *lpc, char *format, ...)
{
    va_list ap;
    char    buf[LPC_BUF_LEN];
    int     len;

    va_start(ap, format);
    len = vsnprintf(buf, LPC_BUF_LEN, format, ap);
    va_end(ap);

    if (len >= LPC_BUF_LEN)
        len = LPC_BUF_LEN - 1;

    return lpc_write(lpc, buf, len);
}

/* get error string */
char *lpc_status(LPC *lpc)
{
    return lpc->status;
}
